查看原文
其他

茴字的三种写法之angr符号执行寻找Flag

xuada 看雪学苑 2022-07-01


本文为看雪论坛优秀文章

看雪论坛作者ID:xuada





前言


因为项目需求,最近在研究如何使用符号执行技术对恶意程序进行分析。查找资料的时候发现国内关于符号执行的技术贴还是非常少的,查找了很多关于符号执行的开源框架之后,最终还是选择使用angr探探路(也只有angr有一些入门教程了(lll¬ω¬))。作为一名第一次发帖的新手,也趁着30天写作挑战活动的势头,发帖记录一下自己初学angr的一些理解与使用吧。





一、angr简介


1.1 angr的定义


[angr的官方文档](https://docs.angr.io/)对于angr的定义如下:

angr是一个多体系结构的二进制分析工具包,能够执行动态符号执行(如Mayhem、kle等)和二进制文件的各种静态分析。

1.2 对angr的理解

我个人对angr的理解是angr使用符号执行的技术,可以静态的遍历程序的每一个分支,模拟运行这个分支的时候的内存、寄存器实时的变化,**并且能够找到触发该分支的输入内容**。

虽然angr理论上应该对恶意程序的分析是可行的,但是作为刚接触符号执行这个概念的新手,还是决定先试试使用angr获取简单的crack程序的flag。




二、05_angr_symbolic_memory


2.1 程序初探


由于论坛里已经有前4道题目题解,因此在这里分享一下使用angr对第五道题目的多种解法。首先运行一下 :

发现有四段输入,再使用file命令看一下架构:

好的32位,直接用idaPro打开。

虽然首先看看汇编码来理解程序是个好习惯,但是为了行文方便,直接F5反编译:

int __cdecl main(int argc, const char **argv, const char **envp){ signed int i; // [esp+Ch] [ebp-Ch] memset(user_input, 0, 0x21u); printf("Enter the password: "); __isoc99_scanf("%8s %8s %8s %8s", user_input, &unk_A1BA1C8, &unk_A1BA1D0, &unk_A1BA1D8); for ( i = 0; i <= 31; ++i ) *(_BYTE *)(i + 169583040) = complex_function(*(char *)(i + 169583040), i); if ( !strncmp(user_input, "NJPURZPCDYEAXCSJZJMPSOMBFDDLHBVN", 0x20u) ) puts("Good Job."); else puts("Try again."); return 0;}


可以看出scanf分别接收了4个8字节长的字符串,连一起正好32字节,字符串存在程序.bss节,也就是说scanf接受的32字节长的字符串将不会存于栈而是存于事先就声明的未初始化的全局变量之中。

随后for循环迭代32次,对输入的字符串的每个字节进行complex_function函数的加密处理,最后将加密过后的字符串与'NJPURZPCDYEAXCSJZJMPSOMBFDDLHBVN'进行比较,字符串相等则算是拿到了flag。

整个程序的结构逻辑还是非常简单清晰的,接下来启动angr,载入程序。

2.2 angr执行之地址搜索法


angr的安装非常非常非常简单无痛,在virtualenv 环境下pip install angr就ok了。首先启动Jupyter Notebook,启动angr载入要分析的二进制文件:

import angr,claripy,sysproj = angr.Project('05_angr_symbolic_memory')init_state = proj.factory.entry_state()

Project对象只代表程序的“初始化映像”。若要使用angr进行符号执行,需要一个特定的对象State表示模拟程序的状态,当前选用了state对象表示模拟从程序的入口点开始的状态。

随后我们需要找到符号执行的终点站,也就是正确的flag的位置,使用idaPro看一下:

.text:0804865B push offset s ; "Try again.".text:08048660 call _puts.text:08048665 add esp, 10h.text:08048668 jmp short loc_804867A.text:0804866A ; ---------------------------------------------------------------------------.text:0804866A.text:0804866A loc_804866A: ; CODE XREF: main+AE↑j.text:0804866A sub esp, 0Ch.text:0804866D push offset aGoodJob ; "Good Job.".text:08048672 call _puts.text:08048677 add esp, 10h

我们发现'Good Job.'的地址位于0x0804866D,'Try again.'的地址位于0x0804865B,接下来在jupyer中定义angr要达到的程序的地址,并申明state的模拟管理器sim来用于执行模拟的程序,使其从一个给定地址的状态到达下一个地址的状态。

good_addr = 0x0804866Abad_addr = 0x0804865Bsim = proj.factory.simgr(init_state)

接下来要模拟程序的执行:

sim.explore(find=good_addr,avoid=bad_addr)

从函数的参数名称就能够知道,需要达到的是good_addr的指令状态,需要剪枝的是bad_addr指令状态,这样是为了避免不必要的状态的搜索,从而极大减少了符号执行需要模拟执行的指令范围以及时间。


运行完成之后,我们得到了如下结果:
<SimulationManager with 1 active, 64 deadended, 1 found>

很好,程序找到了一个可以达到good_addr的情况,我们将angr在到达这个分支所输入的内容打印出来:

solution = sim.found[0]solution.posix.dumps(0)

结果如下:

b'NAXTHGNR JVSFTPWE LMGAUHWC XMDCPALU',刚好四个字符串,每个8个字节长,程序真实运行并输入以上字符串。

说明angr的输入是正确的,我们拿到了真实的flag。

让angr寻找到达具体地址的方法是一个不错的符号执行寻找flag的方法,除了地址还有其他方法吗,答案是有的,angr的强大之处不仅仅是可以搜索达到地址条件的状态,还可以根据多种多样的约束条件进行执行。接下来是根据程序的标准输出进行判断搜索。


2.3 angr执行之标准输入搜索法


之前的载入二进制文件的代码不需要改变,我们定义两个根据程序的标准输出进行判断的程序:

def is_success(state): std_out = state.posix.dumps(1) if b'Good Job.' in std_out: return True else : return Falsedef is_false(state): std_out = state.posix.dumps(1) if b'Try again.' in std_out: return True else: return False

一般情况下,标准输出文件描述符为1,我们对模拟的程序的状态的标准输出进行判断,当Good Job.字符串在标准输出中时候,说明模拟的状态已经到达flag程序分支,angr在该状态中输入的字符串是正确的。

反之,当state输出Try again.,则说明这是要剪枝的分支,没有再进一步模拟执行的必要,从而节省分析时间。让我们模拟运行一下:

sim.explore(find=is_success,avoid=is_false)

得到结果:
 <SimulationManager with 64 deadended, 1 found, 1 avoid>

说明angr找到了能够输出Good Job.的分支,打印该状态的下的输入:

sim.found[0].posix.dumps(0)

b'NAXTHGNR JVSFTPWE LMGAUHWC XMDCPALU'

可见拿到了正确的flag。


2.4 angr执行之内存写入法


之前我们都是直接从二进制程序的程序入口点开始进行模拟的,因为scanf的四个字符串是全局变量,都存与地址已知的内存当中。

我们不妨跳过scanf,直接对这个这四个字符串所在的内存地址进行赋值,让angr直接从scanf之后的程序指令地址开始执行(这也正是angr的强大之处,可以在二进制文件任意的程序地址执行,参数可以任由分析人员修改)。
首先确定模拟程序的入口地址:

.text:080485F4 push offset a8s8s8s8s ; "%8s %8s %8s %8s".text:080485F9 call ___isoc99_scanf.text:080485FE add esp, 20h.text:08048601 mov [ebp+var_C], 0

调用scanf再还原栈地址后,地址为0x08048601,那么让模拟程序从该地址开始,由于不再是从一般程序的入口点开始,所以需要使用blank_state,对载入地址进行自定义:

start_addr = 0x08048601init_state = proj.factory.blank_state(addr=start_addr)

接下来确定scanf存入字符串的内存的地址,并申明4个8字节长的字符串向量。

password3_address = 0x0A1BA1D8password2_address = 0x0A1BA1D0password1_address = 0x0A1BA1C8password0_address = 0x0A1BA1C0password0 = claripy.BVS('password0',64)password1 = claripy.BVS('password1',64)password2 = claripy.BVS('password2',64)password3 = claripy.BVS('password3',64)

将四个字符串载入模拟的程序状态的内存之中:

init_state.memory.store(password0_address,password0)init_state.memory.store(password1_address,password1)init_state.memory.store(password2_address,password2)init_state.memory.store(password3_address,password3)

开始模拟执行:

sim = proj.factory.simgr(init_state)sim.explore(find=is_success,avoid=is_false)

得到结果:
 <SimulationManager with 1 found, 65 avoid>

查看在状态内存中输入的四个字符串向量的值:

pass0 = solution_state.se.eval(password0,cast_to=bytes)pass1 = solution_state.se.eval(password1,cast_to=bytes)pass2 = solution_state.se.eval(password2,cast_to=bytes)pass3 = solution_state.se.eval(password3,cast_to=bytes)print(pass0,pass1,pass2,pass3)

b'NAXTHGNR' b'JVSFTPWE' b'LMGAUHWC' b'XMDCPALU'

OK,圆满完成。




三、总结


由于时间关系,就简单的写下angr对这个题目三种解法,其实方法还很多,我就算是抛砖引玉了。

由于初学angr,肯定很多概念说的含混不清甚至有错,非常欢迎大家在评论区留言评论。另外,如果有大佬有关于恶意软件符号执行分析的经验或者想法,如果可以的话,也烦请留言提点一下(o゚v゚)ノ


ps.  05_angr_symbolic_memory题目请点击“阅读原文”下载。



- End -



看雪ID:xuada

https://bbs.pediy.com/user-home-829811.htm

  *本文由看雪论坛 xuada 原创,转载请注明来自看雪社区。





本文参与了#看雪30天发帖打卡挑战#活动。


发帖见证成长,坚持见证不凡。


不仅可以收获进步,还可赢取物质奖励哦!


想要了解更多活动详情,戳 ↓



另外,贴心提示本次再印刷还没购买0day安全正书籍的小伙伴抓紧时间啦!


目前已热销493本满500本就可安排啦!


点击以下小程序即可预购!


推荐文章++++

* 反汇编代码还原之加减乘

* 基于inlinehook免重打包实现持久化NativeHook

* 反汇编代码还原之优化方式

* 从钓鱼邮件到窃密木马的完整分析

* 记使用Trace还原ollvm混淆的函数







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



求分享

求点赞

求在看


“阅读原文”一起来充电吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存